Skip to content

feat: show tickets page#170

Merged
hmbanan666 merged 2 commits into
mainfrom
tickets
Sep 19, 2025
Merged

feat: show tickets page#170
hmbanan666 merged 2 commits into
mainfrom
tickets

Conversation

@hmbanan666
Copy link
Copy Markdown
Collaborator

@hmbanan666 hmbanan666 commented Sep 19, 2025

Summary by CodeRabbit

  • New Features

    • Added a Tickets section with a list view, ticket detail page, ticket card and message components, and Telegram deep-linking to a specific ticket.
    • New server endpoint to fetch open tickets.
  • Improvements

    • Faster data refresh interval (20s).
    • Tickets and messages sorted by most recent activity.
    • Minor UI enhancement with an additional icon on the home page.
  • Localization

    • Added Russian translations for “Tickets” and “Show more.”

@hmbanan666 hmbanan666 self-assigned this Sep 19, 2025
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Sep 19, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Replaces Epic references with a Tickets feature: adds ticket store, pages, components, server API and DB ordering; updates navigation and startapp routing; switches app refresh from epic to ticket store and reduces interval from 30000ms to 20000ms; adds RU i18n keys and a small landing-page icon.

Changes

Cohort / File(s) Summary
Tickets feature (store, pages, components, API, repository)
apps/atrium-telegram/app/stores/ticket.ts, apps/atrium-telegram/app/pages/ticket/index.vue, apps/atrium-telegram/app/pages/ticket/[ticketId]/index.vue, apps/atrium-telegram/app/components/TicketCard.vue, apps/atrium-telegram/app/components/TicketMessage.vue, apps/atrium-telegram/server/api/ticket/list.get.ts, packages/database/src/repository/ticket.ts
Adds useTicketStore and TicketWithData type; new ticket list/detail pages; TicketCard and TicketMessage components; server route GET /api/ticket/list returning repository.ticket.listOpened(); repository now orders tickets by updatedAt desc and messages by createdAt desc.
App init / periodic refresh & navigation
apps/atrium-telegram/app/app.vue, apps/atrium-telegram/app/composables/useNavigation.ts, apps/atrium-telegram/app/pages/startapp.vue
Replaces epic store usage with ticket store in app init and periodic update; interval changed 30000→20000 ms; replaces Epic mainRoutes entry with Ticket (/ticket, names ticket, ticket-ticketId); adds ticket case in startapp.vue to navigate to /ticket/{value}.
Localization
apps/atrium-telegram/i18n/locales/ru-RU.json
Adds common.show-more ("Показать еще") and app.tickets ("Тикеты") entries.
Landing page visual tweak
apps/atrium-telegram/app/pages/index.vue
Inserts a centered i-lucide-route icon block under the Flow section.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant A as App (Nuxt)
  participant TS as ticketStore
  participant API as /api/ticket/list
  participant Repo as repository.ticket

  U->>A: Open Tickets UI
  A->>TS: TS.update()
  TS->>API: GET /api/ticket/list (Authorization: tma ...)
  API->>Repo: listOpened()
  Repo-->>API: Tickets ordered (updatedAt desc, messages createdAt desc)
  API-->>TS: JSON tickets[]
  TS-->>A: tickets state updated
  A-->>U: Render Ticket list / details
  Note over A,TS: Periodic refresh every 20s (was 30s)
Loading
sequenceDiagram
  autonumber
  actor U as User
  participant R as startapp.vue
  participant Nav as Router

  U->>R: Open via tgWebAppStartParam
  R->>R: switch(startParam.key)
  alt key == "ticket"
    R->>Nav: navigate to /ticket/{value}?query
  else
    R->>Nav: existing route handling (other keys)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

A rabbit hops through code and queues,
Tickets peek out with bright new views.
Messages stack, timestamps align,
Twenty-second ticks — the logs refine.
I nibble keys and route the tracks, 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "feat: show tickets page" is concise and accurately summarizes the primary change — adding ticket listing and detail pages along with related components and store/API updates. It follows Conventional Commits style and contains no noisy extras. The wording is specific enough for a teammate scanning commit history to understand the main purpose.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 851da16 and 461b98f.

📒 Files selected for processing (1)
  • apps/atrium-telegram/app/components/TicketMessage.vue (1 hunks)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
10.0% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (7)
apps/atrium-telegram/app/app.vue (1)

96-96: Consider the impact of reduced refresh interval.

The refresh interval has been reduced from 30 seconds to 20 seconds. While this provides more frequent updates, it increases server load and client battery consumption.

-  }, 20000)
+  }, 30000)

Consider whether the 10-second reduction is necessary, or if adaptive polling based on user activity would be more efficient.

apps/atrium-telegram/app/components/TicketCard.vue (2)

17-17: Fix potential runtime error with optional chaining

The optional chaining on line 17 is inconsistent with the prop type. Since ticket is a required prop of type TicketWithData which guarantees messages is an array, the optional chaining is unnecessary. However, if messages could be undefined in practice, you should use ticket.messages?.length ?? 0 to avoid displaying undefined.

-          <p>{{ ticket?.messages.length }}</p>
+          <p>{{ ticket.messages?.length ?? 0 }}</p>

24-24: Consider extracting date format string as a constant

The hardcoded Russian text "обновлен" in the format string makes the component less maintainable and not internationalization-friendly. Consider using the i18n system for this label.

-        v-text="format(new Date(ticket.updatedAt), 'обновлен d MMMM yyyy', { locale: ru })"
+        v-text="`${$t('common.updated')} ${format(new Date(ticket.updatedAt), 'd MMMM yyyy', { locale: ru })}`"

Also add the translation key to your i18n files:

{
  "common": {
    "updated": "обновлен"
  }
}
apps/atrium-telegram/app/stores/ticket.ts (1)

22-24: Consider more explicit handling of empty responses

The current check if (!data) only handles falsy values but doesn't distinguish between an empty array (valid response with no tickets) and an actual error condition. Consider being more explicit about what constitutes a valid response.

-      if (!data) {
-        return
-      }
+      if (!Array.isArray(data)) {
+        console.warn('Unexpected response format from /api/ticket/list')
+        return
+      }
apps/atrium-telegram/app/components/TicketMessage.vue (1)

119-130: Add proper TypeScript type safety for file type parameter

The getFileIcon function accepts a union type including null, but the implementation doesn't explicitly handle the null case (it falls through to default). While this works, it's better to be explicit for clarity and type safety.

-function getFileIcon(type: 'image' | 'video' | 'document' | null) {
+function getFileIcon(type: 'image' | 'video' | 'document' | null): string {
   switch (type) {
     case 'image':
       return 'i-lucide-image'
     case 'video':
       return 'i-lucide-video'
     case 'document':
       return 'i-lucide-file'
+    case null:
     default:
       return 'i-lucide-file'
   }
 }
apps/atrium-telegram/app/pages/ticket/[ticketId]/index.vue (2)

20-20: Handle edge case for undefined ticket in pluralization

The pluralization function receives ticket?.messages.length ?? 0 but the template also references ticket?.messages.length directly without fallback, which could display undefined.

-        {{ ticket?.messages.length }} {{ pluralizationRu(ticket?.messages.length ?? 0, ['сообщение', 'сообщения', 'сообщений']) }}
+        {{ ticket?.messages.length ?? 0 }} {{ pluralizationRu(ticket?.messages.length ?? 0, ['сообщение', 'сообщения', 'сообщений']) }}

44-58: Clean up commented code

There's a large block of commented code for a drawer component. If this feature is planned for future implementation, consider adding a TODO comment explaining when it will be implemented. Otherwise, remove it to keep the codebase clean.

Either remove the commented code entirely or replace with a clear TODO:

-    <!-- <UDrawer v-model:open="isDrawerOpened">
-      <CreateCard
-        v-if="epic?.id"
-        :label="$t('app.create.epic-comment.button')"
-        icon="i-lucide-message-circle"
-      />
-
-      <template #body>
-        <FormCreateEpicComment
-          :epic-id="epic?.id ?? ''"
-          @submitted="isDrawerOpened = false"
-          @success="isDrawerOpened = false"
-        />
-      </template>
-    </UDrawer> -->
+    <!-- TODO: Implement comment creation drawer when FormCreateTicketComment is ready -->
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 80c8ffe and 851da16.

📒 Files selected for processing (12)
  • apps/atrium-telegram/app/app.vue (3 hunks)
  • apps/atrium-telegram/app/components/TicketCard.vue (1 hunks)
  • apps/atrium-telegram/app/components/TicketMessage.vue (1 hunks)
  • apps/atrium-telegram/app/composables/useNavigation.ts (1 hunks)
  • apps/atrium-telegram/app/pages/index.vue (1 hunks)
  • apps/atrium-telegram/app/pages/startapp.vue (1 hunks)
  • apps/atrium-telegram/app/pages/ticket/[ticketId]/index.vue (1 hunks)
  • apps/atrium-telegram/app/pages/ticket/index.vue (1 hunks)
  • apps/atrium-telegram/app/stores/ticket.ts (1 hunks)
  • apps/atrium-telegram/i18n/locales/ru-RU.json (1 hunks)
  • apps/atrium-telegram/server/api/ticket/list.get.ts (1 hunks)
  • packages/database/src/repository/ticket.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
packages/database/src/repository/ticket.ts (1)
packages/database/src/tables.ts (2)
  • tickets (689-701)
  • ticketMessages (703-719)
apps/atrium-telegram/app/stores/ticket.ts (3)
packages/database/src/repository/ticket.ts (2)
  • Ticket (6-76)
  • update (61-71)
packages/database/src/types.ts (1)
  • TicketMessage (182-182)
packages/database/src/tables.ts (1)
  • tickets (689-701)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (11)
apps/atrium-telegram/app/pages/startapp.vue (1)

30-32: LGTM! The routing implementation is consistent.

The new ticket case follows the established pattern and correctly routes to /ticket/${value} with the proper query parameter, maintaining consistency with existing flow and epic routes.

apps/atrium-telegram/app/pages/index.vue (1)

38-40: Stylistic addition for visual consistency.

The centered icon matches the design pattern from the tickets page, providing visual consistency across the application.

apps/atrium-telegram/app/composables/useNavigation.ts (1)

19-23: Navigation update aligns with the tickets feature.

The route replacement from /epic to /ticket is consistent with the overall migration to a ticket-based system. The icon choice i-lucide-mail-question-mark appropriately represents a ticketing system.

packages/database/src/repository/ticket.ts (2)

20-20: Proper ordering for recent tickets first.

Adding desc(tickets.updatedAt) ensures tickets are displayed with most recently updated ones first, which is appropriate for a ticketing system.


22-24: Consistent message ordering.

Sorting ticket messages by createdAt in descending order maintains chronological consistency, showing the most recent messages first within each ticket.

apps/atrium-telegram/i18n/locales/ru-RU.json (2)

21-21: Translation added for UI functionality.

The "Показать еще" translation supports the show-more functionality mentioned in the AI summary.


26-26: Navigation label translation added.

The "Тикеты" translation provides proper Russian localization for the tickets navigation item.

apps/atrium-telegram/app/app.vue (2)

63-63: Store migration from epic to ticket.

The replacement of useEpicStore() with useTicketStore() aligns with the feature migration to tickets.


82-82: Store update calls consistent with migration.

Both initial and periodic update calls now use ticket.update() instead of epic.update(), maintaining data consistency.

Also applies to: 92-92

apps/atrium-telegram/app/pages/ticket/index.vue (1)

1-22: Well-structured ticket listing page.

The implementation follows Vue 3 best practices with proper key binding, consistent styling with motion-preset-slide-left, and appropriate component organization.

apps/atrium-telegram/app/components/TicketMessage.vue (1)

132-134: Validate file URLs before opening

Searches didn't reveal a shared URL sanitizer; add runtime validation to prevent javascript:, data:, or other unsafe schemes.

Location: apps/atrium-telegram/app/components/TicketMessage.vue lines 132–134

 function handleFileClick(fileUrl: string) {
+  // Validate URL to prevent javascript: or data: URIs
+  try {
+    const url = new URL(fileUrl)
+    if (!['http:', 'https:'].includes(url.protocol)) {
+      console.error('Invalid file URL protocol')
+      return
+    }
+  } catch {
+    console.error('Invalid file URL')
+    return
+  }
   window.open(fileUrl, '_blank')
 }

color: 'neutral',
disabled: false,
onSelect: () => navigator.clipboard.writeText(message.value?.text ?? ''),
condition: true,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Ensure proper handling of undefined condition property

The TypeScript type DropdownMenuItem might not include a condition property based on the Nuxt UI types. This could cause TypeScript errors or runtime issues.


🌐 Web query:

Does the DropdownMenuItem type from @nuxt/ui include a condition property?

💡 Result:

No. The official DropdownMenuItem type in @nuxt/ui does not include a "condition" property — the documented item properties (label, icon, avatar, kbds, type, color, checked, disabled, slot, onSelect, onUpdateChecked, children, class, ui, etc.) do not list "condition". [1][2]


Remove/guard unknown "condition" on DropdownMenuItem objects

The @nuxt/ui DropdownMenuItem type does not include a "condition" property; keeping it in an object literal will cause TypeScript errors.

  • Fix: apps/atrium-telegram/app/components/TicketMessage.vue — remove condition: true (line 82) and compute/filter visibility before creating DropdownMenuItem objects.
  • Alternative: if you need the field, map items to the library's shape or explicitly cast/augment the type (e.g., as DropdownMenuItem & { condition?: boolean } or module augmentation).
🤖 Prompt for AI Agents
In apps/atrium-telegram/app/components/TicketMessage.vue around line 82, the
DropdownMenuItem objects include an unsupported "condition" property which
causes TypeScript errors; remove the `condition: true` entry from the object
literal and instead compute visibility before constructing the DropdownMenuItem
array (filter items based on your condition), or if you must keep the field for
internal logic map your items into the library's expected shape or explicitly
cast/augment the type (e.g., map to DropdownMenuItem & { condition?: boolean })
so the final objects passed to the DropdownMenu accept the correct type.

Comment on lines +74 to +76
const shownMessages = ref(10)
const messages = computed(() => ticket.value?.messages.slice(0, shownMessages.value))
const isShowMore = computed<boolean>(() => messages.value?.length && ticket.value?.messages.length ? messages.value.length < ticket.value.messages.length : false)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Simplify computed property logic and fix potential edge cases

The messages computed property could return undefined if ticket is not found, and the isShowMore logic is unnecessarily complex with redundant checks.

-const messages = computed(() => ticket.value?.messages.slice(0, shownMessages.value))
-const isShowMore = computed<boolean>(() => messages.value?.length && ticket.value?.messages.length ? messages.value.length < ticket.value.messages.length : false)
+const messages = computed(() => ticket.value?.messages.slice(0, shownMessages.value) ?? [])
+const isShowMore = computed<boolean>(() => {
+  if (!ticket.value?.messages.length) return false
+  return messages.value.length < ticket.value.messages.length
+})

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/atrium-telegram/app/pages/ticket/[ticketId]/index.vue around lines
74-76, the messages computed may return undefined when ticket or ticket.messages
is missing and isShowMore has redundant checks; change messages to always return
an array (e.g., use (ticket.value?.messages ?? []).slice(0,
shownMessages.value)) and simplify isShowMore to compare lengths against a safe
number (e.g., messages.value.length < (ticket.value?.messages?.length ?? 0)) so
it handles missing data and removes unnecessary conditional logic.

Comment on lines +28 to +36
if (error instanceof Error) {
if (error.message.includes('401')) {
// No session
}
if (error.message.includes('404')) {
// Not found
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Improve error handling implementation

The current error handling checks for status codes via string matching in error messages, which is fragile and non-standard. Additionally, the error handlers don't perform any actions, making them ineffective.

-    } catch (error) {
-      if (error instanceof Error) {
-        if (error.message.includes('401')) {
-          // No session
-        }
-        if (error.message.includes('404')) {
-          // Not found
-        }
-      }
+    } catch (error: any) {
+      // Handle FetchError from $fetch
+      if (error?.data?.statusCode === 401 || error?.statusCode === 401) {
+        // No session - could redirect to login or show auth prompt
+        console.error('Authentication required for ticket access')
+        tickets.value = []
+      } else if (error?.data?.statusCode === 404 || error?.statusCode === 404) {
+        // Not found - likely means no tickets endpoint
+        console.error('Ticket endpoint not found')
+        tickets.value = []
+      } else {
+        // Log unexpected errors
+        console.error('Failed to fetch tickets:', error)
+        throw error // Re-throw to let caller handle if needed
+      }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (error instanceof Error) {
if (error.message.includes('401')) {
// No session
}
if (error.message.includes('404')) {
// Not found
}
}
}
} catch (error: any) {
// Handle FetchError from $fetch
if (error?.data?.statusCode === 401 || error?.statusCode === 401) {
// No session - could redirect to login or show auth prompt
console.error('Authentication required for ticket access')
tickets.value = []
} else if (error?.data?.statusCode === 404 || error?.statusCode === 404) {
// Not found - likely means no tickets endpoint
console.error('Ticket endpoint not found')
tickets.value = []
} else {
// Log unexpected errors
console.error('Failed to fetch tickets:', error)
throw error // Re-throw to let caller handle if needed
}
}
🤖 Prompt for AI Agents
In apps/atrium-telegram/app/stores/ticket.ts around lines 28-36, the code is
doing fragile string-matching on error.message for status codes and no-ops
inside those branches; replace this with robust type-aware checks (e.g., if the
error is an AxiosError or has error.response?.status) and switch on
error.response.status (401, 404) instead of message.includes; inside the 401
branch clear any auth/session state and trigger a logout or session-refresh flow
and log the event, and inside the 404 branch set a notFound flag or return a
specific error value so callers can handle it, while for other errors rethrow or
log the full error to preserve diagnostics.

Comment on lines +3 to +5
export default defineEventHandler(async () => {
return repository.ticket.listOpened()
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Critical security concern: API endpoint lacks authentication.

This API endpoint is unprotected and exposes ticket data without any authentication checks. In Nuxt 3, you should implement authentication using server middleware or wrapper functions to validate user sessions before allowing access to sensitive data.

Based on the web search results showing Nuxt 3 authentication patterns, implement authentication protection:

+import { serverSupabaseUser } from '#supabase/server' // or your auth method
 import { repository } from '@roll-stack/database'

-export default defineEventHandler(async () => {
+export default defineEventHandler(async (event) => {
+  // Verify authentication
+  const user = await serverSupabaseUser(event) // or your auth validation
+  if (!user) {
+    throw createError({
+      statusCode: 401,
+      statusMessage: 'Unauthorized'
+    })
+  }
+  
   return repository.ticket.listOpened()
 })

Alternatively, create a reusable protectRoute utility and apply it to endpoints that require authentication.


🏁 Script executed:

#!/bin/bash
# Check if other API endpoints in the project have authentication
rg -n --type=ts -C3 'defineEventHandler' apps/atrium-telegram/server/api/

Length of output: 15387


Critical — protect ticket list endpoint (unauthenticated data exposure).

apps/atrium-telegram/server/api/ticket/list.get.ts returns repository.ticket.listOpened() with no auth check; other handlers rely on event.context.user (e.g. apps/atrium-telegram/server/api/notification/my.get.ts, apps/atrium-telegram/server/api/auth/me.get.ts). Require authentication — either make the route explicitly public and document/tests that decision, or enforce auth by switching to defineEventHandler(async (event) => { if (!event.context?.user) throw createError({ statusCode: 401, statusMessage: 'Unauthorized' }); return repository.ticket.listOpened(); }) or by applying the project’s existing protectRoute/middleware.

🤖 Prompt for AI Agents
In apps/atrium-telegram/server/api/ticket/list.get.ts around lines 3 to 5, the
handler returns repository.ticket.listOpened() with no authentication check
exposing ticket data; update the handler to require auth by accepting the event
parameter and either call the project’s existing protectRoute/middleware or
manually verify event.context?.user and if missing throw createError({
statusCode: 401, statusMessage: 'Unauthorized' }) before returning
repository.ticket.listOpened(); ensure tests or route docs are updated if you
intend to make the route explicitly public instead.

@hmbanan666 hmbanan666 merged commit 495cac9 into main Sep 19, 2025
6 of 8 checks passed
@hmbanan666 hmbanan666 deleted the tickets branch September 19, 2025 09:15
@coderabbitai coderabbitai Bot mentioned this pull request Oct 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant